feat(amicode): approval state machine + ledger transport (warrant surfaces, part 1) - #75
Closed
aarontrowbridge wants to merge 19 commits into
Closed
feat(amicode): approval state machine + ledger transport (warrant surfaces, part 1)#75aarontrowbridge wants to merge 19 commits into
aarontrowbridge wants to merge 19 commits into
Conversation
The pulse chip opens the Run Inspector while still un-banked (766e201), but it kept the dotted border — the exact grammar every OTHER pending chip uses to mean inert — so its clickability was only discoverable on hover. Add an always-visible trailing chevron (neutral muted ink, bumps to base on hover/focus) so the shape signals the action at rest; the dotted border still tells the truth about not-yet-banked state. Also retire the stale comment claiming the recorded chips' soft-yellow fill (removed 2026-07-24). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The solver row said "API key connected" and "Runs in the cloud" — which cloud was never stated, on the one control where a user commits to a paid service. Every refusal they can hit downstream now says "Harmoniqs Cloud" (amico-run's local-launch refusal, the hpc gate), so the control that sends them there should use the same name. The connected tooltip also states the consequence — every solve on this solver runs there — since that is the thing users were guessing about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported: "where is our harmoniqs cloud, where can I put an API key in" — and separately, switching to Piccolissimo + Altissimo appeared to do nothing. Both trace to amicode#200. It filtered company-compute OUT of the Connections tab and renamed it "Solver API key", moving the whole connect flow inside the solver capsule's collapsed card. The reasoning was sound in isolation (one credential for one service, not a second product), but the result was that Pasqal Cloud rendered as a connectable service in the place that lists clouds and OURS did not. Users went looking for it exactly where Pasqal is. - statusTabConnections() no longer filters. Harmoniqs Cloud renders as a card beside Pasqal Cloud, with the base-url-token form (Service URL + API key) the tab already supported for non-Pasqal ids — so this is un-hiding a working flow, not building a new one. Kept as a function so there is still one obvious place to filter a genuinely internal connection later. - connectionTitle() returns "Harmoniqs Cloud". In a list whose other entry is "Pasqal Cloud", "Solver API key" read as a settings field rather than our service — and Harmoniqs Cloud is the name every downstream refusal already uses (amico-run's local-launch refusal, the hpc gate). - The solver row now states its connection state (CONNECTED / CHECK KEY / ADD KEY). With a key already on file, clicking HP correctly activates without re-asking — but the status dot lived inside the collapsed card, so the row had no way to say so and a successful switch was indistinguishable from a no-op. Text rather than a bare dot: Kate's idiom is that the dot accompanies words, and words survive colour-blindness and a screenshot. It also says what a click will do. The wire id stays company-compute — renaming it would break the credential route, the status cache, and CONNECTION_IDS for no user-visible gain. Both entry points write the same credential, so connecting in either shows up in both. ui: 57 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Not every tree node was openable, but nothing said so: turn nodes wore a pointer cursor that clicked into nothing, and files in non-browsable (proprietary) vaults dead-ended in a Vault-panel refusal after the click. - GET /amicode/vaults now stamps each mount with `browsable`, computed by the vault-browser's fail-closed law (deployment gate + kind/marker rules) - tree leaves in a non-browsable mount render locked: dimmed dot, padlock glyph by the label, not-allowed cursor, click is a no-op, and keyboard nav announces "locked — this vault does not allow browsing" - cursor law is now honest everywhere: pointer only where a click opens something; grab (pan) on turns/skills/agents/actions; the click ring only fires on openable nodes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jack-champagne
force-pushed
the
fix/amicode-receipt-currency
branch
from
July 28, 2026 22:52
78ee045 to
7639a8f
Compare
…ne inspector entry The chip and the button opened the same panel from the same row; with the chip now carrying an at-rest affordance, the button was redundant chrome (Kate 2026-07-28). Comments referencing the button updated to name the pulse chip as the rail's inspector entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
formatFidelity's toFixed(4) collapsed any F ≥ 0.99995 to "1" in the run chip, ring-2 Run hero, and Run modal — hiding exactly the number that matters. Precision now extends a digit at a time (cap 10) until the gap from 1 (or 0) survives rounding; ordinary values keep the 4-decimal form and only a true 1 renders bare. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
style(rail): at-rest chevron on the pending-but-clickable pulse chip
fix(run): fidelity readout never rounds a near-unity F to a bare 1
… open With the context-tree block open the sticky header grows to ~300px and the band sits inside the pl-4 gutter — backdrop-blur(10px) samples transcript content through that transparent 16px strip and smears it into a grey streak along the straight left edge (worst in light theme). The band is opaque below the 48px title strip, so the blur contributed nothing there. Slim headers keep the frosted fade. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(session): header backdrop-blur off while the context-tree band is open
…ance fix(amicode): land #84 on mainline (locked vault context) — stacked-merge miss
fix(amicode): superseded receipts render their captured diff, not the live view
feat(connections): Harmoniqs Cloud is connectable where Pasqal is
…t tests (#98) The three `session.llm native recorded` scenarios are the only failures on `local/amicode`, so the fork's `test` job is red and every PR into it inherits a red check — including #93, whose own suite is 3366 pass / 3 fail with zero failures of its own. Cause is ours. AMICODE-PATCHES.md ADDENDUM 2 consolidated the prompt so `system.ts provider()` returns `[providerBase(model), PROMPT_COMMUNICATING]`, appending `communicating.txt` for every model family. The cassettes still record the pre-patch prompt, so all three break on a diff of prose. Those tests assert TRANSPORT mechanics — that a tool loop is driven to a final text answer. The prompt is incidental, so matching on it couples them to text they do not test. Patching the recorded prompt instead would work until the next prompt edit, and that patch already has three addenda. `RecorderOptions.match` is an existing seam (types.ts:85, threaded through `http()` to `recordingLayer`) that nothing used. Adds `promptAgnosticMatcher` alongside `defaultMatcher` and passes it from this one test. The prompt arrives in three shapes, all covered: instructions OpenAI Responses system[] Anthropic Messages input[].role=="system" the OpenCode proxy System-message content is replaced with a placeholder rather than deleted, so "a system message is present, with content" stays part of the match. Verified narrow, in test/matching.test.ts: it ignores prompt prose in all three shapes, and still rejects a different user message, model, tool set, URL, or a missing-vs-present system message. 49 pass in http-recorder; the three recorded scenarios go 3 fail -> 3 pass; typecheck green across 23 packages.
…s inventing one (#93) * feat(amicode): per-component physics rows on the system card The card made its component-count claim implicitly, so an unanswered 'how many atoms?' read as 'one'. systemCountLabel now states it as its own badge ('2 atoms x 3 levels') that a researcher can read and correct. componentPhysicsRows renders what each role actually has, in card order: - unanswered params read 'not set', so the list doubles as the interview's to-do - anything else on file is still shown, so nothing recorded is dropped - an unrecognized role expects NOTHING -- the honest floor for a platform we have no model for - units are never assumed (transmon params are GHz, the Rydberg templates work in rad/us, a bare omega says which only by convention), so a unit appears only when the recorded key spells it out - a zero keeps the '0 means unset' reading, so an all-zeros seed does not look like a specified device driftTerm now keys the qubit ladder off LEVELS rather than off whether delta happens to be filled in: a 3-level transmon with empty params is still an anharmonic ladder, and rendering it as a bare spin misstates the model the solve will use. A mode's Kerr stays params-driven, since a linear cavity genuinely has none. * feat(amicode): extend the system card's physics rows Continues 0793fd6. Committed on the author's behalf as part of folding the worktree's in-progress work onto the branch; the content is theirs. +169 lines of tests alongside +278 of implementation; ui suite 442 -> 448 pass. * feat(amicode): render the recorded Hamiltonian; stop inferring one off-template The card re-derived the Hamiltonian in the renderer from (role, levels, platform) using hardcoded tables, so it could only ever be right for platforms someone had hardcoded. Two separate leaks put the transmon model on a spin qubit: - `platformDefaultRole` maps every unfamiliar platform to "qubit", so an exchange-only HRL spin qubit arrived indistinguishable from a transmon. - levels >= 3 was read as "anharmonic ladder". It is not. Three levels is a Hilbert-space DIMENSION: an exchange-only qubit at levels=3 is three dots, a spin-1 defect is three Zeeman sublevels. Neither is an oscillator. So the platform is now the only thing that licenses a ladder for a `qubit` role (transmon, bosonic). levels=2 still earns the generic two-level model, which is safe on any platform — every two-level system has an ω σ_z/2 splitting and σx/σy control. Everything else is opaque. Recorded beats inferred. `systemHamiltonian()` returns {latex, source}: a model recorded on the entity (amicode_set_model's `hamiltonian`, see harmoniqs/amicode#220) renders verbatim; otherwise the canonical form for the platform renders under an "inferred · confirm or correct" label. When nothing is recorded AND nothing is modelled the card says "not recorded" rather than showing `Ĥ_drift + Ĥ_c(t)`, which is true of every control problem ever posed and would occupy the slot the real model belongs in. Mixed systems still get a named placeholder per unmodelled component. One `classify()` now drives both the equation and the physics rows, so the two halves of the card cannot describe different physics again. Also fixes a three-way drift found while doing this: the ladder drive was a single `ε(t)(â+â†)` while the plugin's TRANSMON_LATEX (shown in chat) and Piccolo's n_drives = 2 both say two quadratures. Two is correct; the card now matches. Verified: 399 pass across 30 files, tsgo clean. The KaTeX sweep now crosses the platform axis too (8k systems) and asserts BOTH directions: no output when nothing is modelled, and never a model invented for an all-unmodelled system. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(amicode): dedupe the KaTeX sweep — render each distinct equation once The sweep enumerates ~8k systems, but they collapse onto a few hundred distinct equations, and KaTeX is the expensive part. Rendering the same string 200 times proves nothing and took 22s on CI against a 5s default timeout (it passed locally on a fast machine, and I had only been running src/amicode — CI runs all of src). Coverage is unchanged: every distinct output is still parsed, and the mapping back to a producing system is kept for the failure message. Adds an assertion that the sweep really does vary its output, so a future change that collapses everything to one equation can't quietly pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Spec-20260727-164748 §9.5. Two of S3's three modules, mirroring the repo's
existing ask.ts / ask-bridge.ts / ask-card.tsx split. The .tsx component is
NOT here — see the gap note below.
approval.ts (pure): the card's state machine over (request, warrants, now,
hasBridge) → unavailable | pending | granted | expired. Two inherited
properties and one deliberate divergence, all from §9.5:
- Inherits the read-only interlock: no transport → `unavailable`, never
actionable. Absence of a host is the safe state, so it is the default.
- Inherits state-derived-from-the-durable-log: the state is a function of the
approval rows that exist, never a stored UI flag, so it is replay-correct
the way ask-card's hasUserReplyAfter is.
- Does NOT judge whether granted bounds COVER the request. That verdict is
the gate's (§5.1 rule 2); a card that second-guessed it would either
contradict the gate or imply an authority it lacks. The card reports what
was granted, the gate decides what that permits.
Fail-closed twice over: an expiry exactly at `now` reads as expired, and an
UNPARSEABLE expiry reads as expired rather than live — same direction the gate
applies to an unresolved estimate (§4.4). boundsText renders only declared
bounds, since an absent bound is not "unlimited".
approval-bridge.ts: a SEPARATE bridge from ask-bridge, and that separation is
the point. ask-bridge's `send` submits text as the user's next chat message;
routing an approval through it would leave the ledger's only provenance reading
"the agent says the user approved". `approve` reaches `amico ledger approve`
(CLI-3) instead — the ledger's single writer. Disposer is registration-aware so
a late unmount cannot wipe a newer bridge.
packages/ui 352 pass (336 + 16 new), tsgo --noEmit clean.
GAP FOUND (recorded as G-9 in the spec): nothing emits an approval REQUEST.
There is no amicode_* tool for the agent to ask for one, so the card has no
trigger. Writing the component before that surface is decided would be
inventing a tool contract silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Spec-20260727-164748 §9.5. Completes the UI half: a gate refusal can now become
a button instead of prose telling the researcher to go run a CLI verb.
The path: amicode_request_approval (amicode-side plugin tool) renders
AmicodeApprovalCard from the tool INPUT — the ask card's pattern, not a sentinel,
because a request is an ASK rather than a record of something that happened.
Pressing Approve goes through approval-bridge → POST /amicode/approve → shells
`amico ledger approve`. The card then REFETCHES rather than optimistically
flipping, so "granted" is always something the ledger said.
Three deliberate differences from the ask card it otherwise mirrors:
1. Transport. ask-bridge submits as the user's next CHAT MESSAGE, which would
mean the agent reads it and writes the ledger row — leaving the only
provenance as "the agent says the user approved". Hence a sibling bridge,
and `issued_by: user:ui` so an audit can tell a press from the CLI.
2. State source. Derived from the LEDGER (is there a live warrant for this
plan_hash?) rather than from message order. Equally replay-correct, survives
reload, and cannot disagree with what the gate will see.
3. No coverage verdict. The card never claims the granted bounds are enough —
that is the gate's call (§5.1 rule 2). It shows requested, and once granted,
what was ACTUALLY granted, which may be narrower. A card reading "approved"
over a warrant the gate will still refuse is worse than no card.
Server side keeps the single-writer rule (#212): reading the ledger here is a
plain file read, but writing shells the CLI. An append from this process would
break O_APPEND atomicity quietly and only under concurrency. Route and card are
both tolerant per row — a malformed warrant is dropped, never defaulted into one.
The bridge registers only when BOTH halves are present; a card that could approve
but not read back its own warrant would show "pending" forever after a press.
Omitting them leaves the card non-actionable, which is the correct behaviour for
a share page or headless host.
ui 378 pass · app 442 pass · opencode server 550 pass / 15 fail, against 538 / 16
on the clean base — the failures are the documented pre-existing widget-src env
set, verified by stashing. tsgo clean in ui and app.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two halves of the same clutter, seen in a live session: five identical "Recommend updated ✓" chips in a row. Cause was NOT the INLINE_KINDS change — `recommend` was never an inline kind, so it has always rendered as a chip. It read as identical five times because amicode_recommend emits no diff sentinel, so the chip had nothing to distinguish one call from the next, and the interview fires one call per knob. What changed is only that the System cards above them shrank, so a run that used to be buried became the tallest thing on screen. The plugin side (amicode) now emits a sentinel naming the param, so the run reads `Recommend · N 100` / `Recommend · Q 1e5` rather than five copies of one line. Five DISTINCT lines are a record; five identical ones are noise. That alone would have introduced a worse bug: Chip's clickable() was `!!parts()`, so any sentinel made the chip openable — and entity-view.tsx has cases for only the five real kinds, so clicking a `recommend` receipt would have opened an empty dialog. Clickability is now gated on the kind actually having an entity view. Diff detail and openability are separate properties and were conflated. Also records a gap in the spec's §9.3: it specifies at most one EXPANDED card and says nothing about runs of identical COLLAPSED receipts — the same defect one tier down. Coalescing a run (the repo already has AnimatedCountList for exactly this shape) is the follow-up, but naming them first may make hiding them unnecessary. ui 378 pass, tsgo clean. extension 777 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Spec §9.6 / G-6, which settled the rail over the context-tree panel on Kate's own ADR 0003 framing: the panel holds "a map of distinct context, not a log", and warrant bounds are authorization state rather than context. Smaller than the spec implied, because building the approval card already plumbed warrants into the rail as a prop — the "adds a second data source" objection was resolved as a side effect. What actually remained was the solve count and the chip. `/amicode/warrants` now counts `solve` rows per plan in the SAME ledger pass that collects the approvals, so the numerator can never be a stale second counter (§4.5: the ledger is the count-things store). `solves_used` is optional end to end — a surface that does not supply it gets a chip with no count rather than a wrong "0 of 8". railWarrantChip returns UNDEFINED when there is nothing true to say: no live warrant, or a live one declaring no bounds. A chip reading "warranted" with nothing behind it would imply an authorization the gate does not grant, since §5.1 rule 2 refuses a launch needing a bound the warrant omits. Silence is the honest state. The chip is inert on purpose, and reports CONSUMPTION rather than permission. "3 of 8 solves" is a ledger fact; whether the next launch passes is the gate's verdict. Same reason the card carries no coverage claim — two surfaces, one rule. ui 384 pass · app 442 pass · opencode warrants 12 pass · tsgo clean in ui and app. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jack-champagne
force-pushed
the
feat/amicode-approval-surfaces
branch
from
July 29, 2026 08:01
37aaac0 to
72b08af
Compare
Member
Author
|
Superseded by #106 — the approval-surfaces commits are all contained in the consolidation (zero unique ahead). If part 2 of warrant surfaces resumes, rebase onto the post-merge local/amicode. Closing; ships via v1.18.10-amicode.1. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two of the three modules behind the approval card. Stacked on #74 — review that first; this diff shows only the delta once it merges.
Nothing imports these yet, so this is inert. It is landed early because the shape is what wants review, not the wiring.
Design:
spec-20260727-164748-amicode-deliberation-layer-capability-warrants§9.5.approval.ts— the state machine(request, warrants, now, hasBridge)→unavailable | pending | granted | expired. Mirrors the existingask.ts/ask-bridge.ts/ask-card.tsxsplit.Two properties inherited from the ask card deliberately:
unavailable, never actionable.ask-bridgealready establishes that questions never submit from read-only surfaces (share page, headless); approvals get the same by construction rather than by a check someone must remember.And one thing it deliberately does not do: it never judges whether the granted bounds cover the request. That verdict is the gate's (§5.1 rule 2). A card that second-guessed it would either contradict the gate or imply an authority it lacks — so it reports what was granted and lets the gate decide what that permits. A narrower warrant still reads
granted, carrying its actual bounds.Fail-closed twice: an expiry exactly at
nowreads expired, and an unparseable expiry reads expired rather than live.approval-bridge.ts— and why it is notask-bridgeask-bridge.sendsubmits text as the user's next chat message. Routing an approval through it would mean the agent reads that message and then writes the ledger row itself, leaving the only durable provenance reading "the agent says the user approved."approvereachesamico ledger approveinstead — the ledger's single writer. The separation is the point; there is a header comment saying not to merge the two bridges.Verification
packages/ui352 pass (336 + 16 new),tsgo --noEmitclean.Known gap
Nothing emits an approval request yet — the card has no trigger (recorded as G-9). The resolution is to derive it from the launch gate's structured refusal, which now exists on the
amico-runside (amicode#218), so no new agent tool surface is needed. The.tsxcomponent follows once that refusal is plumbed across the process boundary.🤖 Generated with Claude Code